joint.ui.Halo = Backbone.View.extend({

    className: 'halo',

    template: 'halo',

    events: {

        'mousedown .clone': 'startCloning',
        'touchstart .clone': 'startCloning',
        'mousedown .fork': 'startForking',
        'touchstart .fork': 'startForking',
        'mousedown .link': 'startLinking',
        'touchstart .link': 'startLinking',
        'mousedown .unlink': 'unlinkElement',
        'mousedown .remove': 'removeElement',
        'touchstart .remove': 'removeElement',
        'mousedown .resize': 'startResizing',
        'touchstart .resize': 'startResizing',
        'mousedown .rotate': 'startRotating',
        'touchstart .rotate': 'startRotating'
    },

    options: {
	tinyTreshold: 40,
	smallTreshold: 80,
	loopLinkPreferredSide: 'top',
	loopLinkWidth: 40,
        // deprecated (better use joint.dia.Paper.options.linkModel)
	linkAttributes: {},
	smoothLinks: undefined
    },

    initialize: function() {

        _.bindAll(this, 'pointermove', 'pointerup', 'render', 'update', 'remove');

        // inform an existing halo that a new halo is being created
        this.options.paper.trigger('halo:create');

	// Update halo when the graph changed.
        this.listenTo(this.options.graph, 'reset', this.remove);
        this.listenTo(this.options.graph, 'all', this.update);
        // Hide Halo when the user clicks anywhere in the paper or a new halo is created.
        this.listenTo(this.options.paper, 'blank:pointerdown halo:create', this.remove);
        this.listenTo(this.options.paper, 'scale', this.update);

        $(document.body).on('mousemove touchmove', this.pointermove);
        $(document).on('mouseup touchend', this.pointerup);

        this.options.paper.$el.append(this.$el);
    },

    render: function() {

        this.options.cellView.model.on('remove', this.remove);

        this.$el.html(joint.templates['halo.html'](this.template));

	this.renderMagnets();

        this.update();

        this.$el.addClass('animate');
        
        // Add the `data-type` attribute with the `type` of the cell to the root element.
        // This makes it possible to style the halo (including hiding/showing actions) based
        // on the type of the cell.
        this.$el.attr('data-type', this.options.cellView.model.get('type'));

	this.toggleFork();

        return this;
    },

    update: function() {

        if (this.options.cellView.model instanceof joint.dia.Link) return;
        
        var bbox = this.options.cellView.getBBox();

        var position = this.options.cellView.model.get('position');
        var angle = this.options.cellView.model.get('angle');
        var size = this.options.cellView.model.get('size');
        
        this.$('.box').text('x: ' + Math.floor(position.x) + ', y: ' + Math.floor(position.y) + ', width: ' + size.width + ', height: ' + size.height + ', angle: ' + Math.floor((angle || 0)));

	this.$el.toggleClass('tiny', bbox.width < this.options.tinyTreshold && bbox.height < this.options.tinyTreshold);
	this.$el.toggleClass('small', !this.$el.hasClass('tiny') && (bbox.width < this.options.smallTreshold && bbox.height < this.options.smallTreshold));

	this.$el.css({

            width: bbox.width,
            height: bbox.height,
            left: bbox.x,
            top: bbox.y

        }).show();

	this.updateMagnets();

	this.toggleUnlink();
    },

    startCloning: function(evt) {

        evt.preventDefault();
        evt.stopPropagation();
        evt = joint.util.normalizeEvent(evt);

	this.options.graph.trigger('batch:start');
        
        this._action = 'clone';

        this._clone = this.options.cellView.model.clone();
        this._clone.unset('z');
        this.options.graph.addCell(this._clone);

        this._clientX = evt.clientX;
        this._clientY = evt.clientY;
    },

    startLinking: function(evt) {

        evt.preventDefault();
        evt.stopPropagation();
        evt = joint.util.normalizeEvent(evt);

	this.options.graph.trigger('batch:start');

        this._action = 'link';

        var cellView = this.options.cellView;
        var selector = $.data(evt.target, 'selector');
        var link = this.options.paper.getDefaultLink(cellView, selector && cellView.el.querySelector(selector));

	link.set('source', { id: cellView.model.id, selector: selector });
        link.set('target', { x: 0, y: 0 });

	link.attr(this.options.linkAttributes);
        if (_.isBoolean(this.options.smoothLinks)) {
            link.set('smooth', this.options.smoothLinks);
        }

	// add link to graph but don't validate
        this.options.graph.addCell(link, { validation: false });

        link.set('target', this.options.paper.snapToGrid({ x: evt.clientX, y: evt.clientY }));

        this._linkView = this.options.paper.findViewByModel(link);
        this._linkView.startArrowheadMove('target');

        this._clientX = evt.clientX;
        this._clientY = evt.clientY;
    },

    startForking: function(evt) {

        evt.preventDefault();
        evt.stopPropagation();
        evt = joint.util.normalizeEvent(evt);

	this.options.graph.trigger('batch:start');
        
        this._action = 'fork';

        this._clone = this.options.cellView.model.clone();
        this._clone.unset('z');
        this.options.graph.addCell(this._clone);

        var link = this.options.paper.getDefaultLink(this.options.cellView);

	link.set('source', { id: this.options.cellView.model.id });
        link.set('target', { id: this._clone.id });

	link.attr(this.options.linkAttributes);
        if (_.isBoolean(this.options.smoothLinks)) {
            link.set('smooth', this.options.smoothLinks);
        }

        this.options.graph.addCell(link);

        this._clientX = evt.clientX;
        this._clientY = evt.clientY;
    },

    startResizing: function(evt) {

        evt.preventDefault();
        evt.stopPropagation();
        evt = joint.util.normalizeEvent(evt);

	this.options.graph.trigger('batch:start');
        
        this._action = 'resize';

        // determine whether to flip x,y mouse coordinates while resizing or not
        this._flip = [1,0,0,1,1,0,0,1][
            Math.floor(g.normalizeAngle(this.options.cellView.model.get('angle')) / 45)
        ];

        this._clientX = evt.clientX;
        this._clientY = evt.clientY;
    },

    startRotating: function(evt) {

        evt.preventDefault();
        evt.stopPropagation();
        evt = joint.util.normalizeEvent(evt);

	this.options.graph.trigger('batch:start');
        
        this._action = 'rotate';

        var bbox = this.options.cellView.getBBox();
        
        this._center = g.rect(bbox).center();

	//mousemove event in firefox has undefined offsetX and offsetY
	if(typeof evt.offsetX === "undefined" || typeof evt.offsetY === "undefined") {
	    var targetOffset = $(evt.target).offset();
	    evt.offsetX = evt.pageX - targetOffset.left;
	    evt.offsetY = evt.pageY - targetOffset.top;
	}

        this._rotationStart = g.point(evt.offsetX + evt.target.parentNode.offsetLeft, evt.offsetY + evt.target.parentNode.offsetTop + evt.target.parentNode.offsetHeight);

        var angle = this.options.cellView.model.get('angle');

        this._rotationStartAngle = angle || 0;

        this._clientX = evt.clientX;
        this._clientY = evt.clientY;
    },
    
    pointermove: function(evt) {

        if (!this._action) return;

        evt.preventDefault();
        evt.stopPropagation();
        evt = joint.util.normalizeEvent(evt);

        var clientCoords = this.options.paper.snapToGrid({ x: evt.clientX, y: evt.clientY });
        var oldClientCoords = this.options.paper.snapToGrid({ x: this._clientX, y: this._clientY });
        
        var dx = clientCoords.x - oldClientCoords.x;
        var dy = clientCoords.y - oldClientCoords.y;
        
        switch (this._action) {

          case 'resize':

            var size = this.options.cellView.model.get('size');

            var width = Math.max(size.width + ((this._flip ? dx : dy)), 1);
            var height = Math.max(size.height + ((this._flip ? dy : dx)), 1);

            this.options.cellView.model.resize(width, height, { absolute: true });

            this._clientX = evt.clientX;
            this._clientY = evt.clientY;
            break;
            
          case 'rotate':

            dx = evt.clientX - this._clientX;
            dy = evt.clientY - this._clientY;
            
            var p = g.point(this._rotationStart).offset(dx, dy);
            var a = p.distance(this._center);
            var b = this._center.distance(this._rotationStart);
            var c = this._rotationStart.distance(p);
            var sign = (this._center.x - this._rotationStart.x) * (p.y - this._rotationStart.y) - (this._center.y - this._rotationStart.y) * (p.x - this._rotationStart.x);

            var _angle = Math.acos((a*a + b*b - c*c) / (2*a*b));

            // Quadrant correction.
            if (sign <= 0) {
                _angle = -_angle;
            }
            
            var angleDiff = -g.toDeg(_angle);

            angleDiff = g.snapToGrid(angleDiff, 15);
            
            this.options.cellView.model.rotate(angleDiff + this._rotationStartAngle, true);
            break;
            
          case 'clone':
          case 'fork':
            
            this._clone.translate(dx, dy);

            this._clientX = evt.clientX;
            this._clientY = evt.clientY;
            break;

          case 'link':

            this._linkView.pointermove(evt, clientCoords.x, clientCoords.y);
            break;
        }
    },

    pointerup: function(evt) {

        if (!this._action) return;

        evt.preventDefault();
        
        switch (this._action) {

          case 'link':

            this._linkView.pointerup(evt);

            var sourceId = this._linkView.model.get('source').id;
            var targetId = this._linkView.model.get('target').id;

	    if (sourceId && targetId && (sourceId === targetId)) {
		this.makeLoopLink(this._linkView.model);
	    }

            break;
	}

	delete this._linkView;
        delete this._action;

	this.options.graph.trigger('batch:stop');
    },

    remove: function(evt) {

	Backbone.View.prototype.remove.apply(this, arguments);

        $(document.body).off('mousemove touchmove', this.pointermove);
        $(document).off('mouseup touchend', this.pointerup);
    },

    removeElement: function(evt) {

        evt.stopPropagation();
        this.options.cellView.model.remove();
    },

    unlinkElement: function(evt) {

        evt.stopPropagation();
        this.options.graph.removeLinks(this.options.cellView.model);
    },

    toggleUnlink: function() {

	if (this.options.graph.getConnectedLinks(this.options.cellView.model).length > 0) {
	    this.$('.unlink').show()
	} else {
	    this.$('.unlink').hide()
	}
    },

    toggleFork: function() {

        // temporary create a clone model and its view
        var clone = this.options.cellView.model.clone();
        var cloneView = this.options.paper.createViewForModel(clone);

        // if a connection after forking would not be valid, hide the fork icon
        if (!this.options.paper.options.validateConnection(this.options.cellView,null,cloneView,null,'target')) {
	    this.$('.fork').hide();
        }

        cloneView.remove();
        clone = null;
    },

    makeLoopLink: function(link) {

	var linkWidth = this.options.loopLinkWidth;
	var paperOpt = this.options.paper.options;
	var paperRect = g.rect({x: 0, y: 0, width: paperOpt.width, height: paperOpt.height});
	var bbox = V(this.options.cellView.el).bbox(false, this.options.paper.viewport);
	var p1, p2;

	var sides = _.uniq([this.options.loopLinkPreferredSide, 'top', 'bottom', 'left', 'right']);
	var sideFound = _.find(sides, function(side) {

	    var centre, dx = 0, dy = 0;

	    switch (side) {

	    case 'top':
		centre = g.point(bbox.x + bbox.width / 2, bbox.y - linkWidth);
		dx = linkWidth / 2;
		break;

	    case 'bottom':
		centre = g.point(bbox.x + bbox.width / 2, bbox.y + bbox.height + linkWidth);
		dx = linkWidth / 2;
		break;

	    case 'left':
		centre = g.point(bbox.x - linkWidth, bbox.y + bbox.height / 2);
		dy = linkWidth / 2;
		break;

	    case 'right':
		centre = g.point(bbox.x + bbox.width + linkWidth, bbox.y + bbox.height / 2);
		dy = linkWidth / 2;
		break;
	    };

	    p1 = g.point(centre).offset(-dx, -dy);
	    p2 = g.point(centre).offset(dx, dy);

	    return paperRect.containsPoint(p1) && paperRect.containsPoint(p2);
	}, this);

	if (sideFound) link.set('vertices', [p1,p2]);
    },

    // Magnet functions

    renderMagnets: function() {

	this._magnets = [];

	var $link = this.$('.link');
	var magnetElements = this.options.cellView.$('[magnet="true"]');

	if (this.options.magnetFilter) {

	    if (_.isFunction(this.options.magnetFilter)) {

		// We want function to be called with a magnet element as the first parameter. Not an index
		// as jQuery.filter would do it.
		magnetElements = _.filter(magnetElements, this.options.magnetFilter);

	    } else {

		// Every other case runs jQuery.filter method
		magnetElements = magnetElements.filter(this.options.magnetFilter);
	    }
	}

	if ($link.length && magnetElements.length) {

	    var linkWidth = $link.width();
	    var linkHeight = $link.height();

	    _.each(magnetElements, function(magnetElement) {

		var magnetClientRect = magnetElement.getBoundingClientRect();

		var $haloElement = $link.clone()
		    .addClass('halo-magnet')
		    .css({
			width: Math.min(magnetClientRect.width, linkWidth),
			height: Math.min(magnetClientRect.height, linkHeight),
			'background-size': 'contain'
		    })
		    .data('selector', this.options.cellView.getSelector(magnetElement))
		    .appendTo(this.$el);

		this._magnets.push({ $halo: $haloElement, el: magnetElement });

	    }, this);
	}

	// disable linking & forking from the element itself if is it not a magnet
	if (this.options.cellView.$el.attr('magnet') == 'false') {
	    $link.hide();
	    this.$('.fork').hide();
	}
    },

    updateMagnets: function() {

	if (this._magnets.length) {

	    var hClientRect = this.el.getBoundingClientRect();

	    // adjust position of each halo magnet
	    _.each(this._magnets, function(magnet) {

		var mClientRect = magnet.el.getBoundingClientRect();

		magnet.$halo.css({
		    left: mClientRect.left - hClientRect.left + (mClientRect.width - magnet.$halo.width())/2,
		    top: mClientRect.top - hClientRect.top + (mClientRect.height - magnet.$halo.height())/2
		});

	    }, this);
	}
    }

});
